package com.avcompris.util;

import static com.avcompris.util.ExceptionUtils.nonNullArgument;
import static com.avcompris.util.log.LogUtils.logDebugFormat;
import static java.lang.Thread.currentThread;
import static org.apache.commons.lang3.CharEncoding.UTF_8;
import static org.apache.log4j.Logger.getLogger;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.Enumeration;
import java.util.Properties;

import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;

import com.avcompris.common.annotation.Nullable;

/**
 * utitlies to read properties from the environment.
 * 
 * @author David Andriana Copyright Avantage Compris SARL 2008-2009 ©
 */

public abstract class PropertiesUtils extends AbstractUtils {

	/**
	 * the logger to use.
	 */
	private static final Logger logger = getLogger(PropertiesUtils.class);

	/**
	 * return a {@link Properties} object, found either amongst the System
	 * properties, or in a property file. If the property is still of the form "
	 * <tt>${...}</tt>", this method raises an exception.
	 * 
	 * @param clazz
	 *            the Java class from which to search for the property file
	 * @param filename
	 *            the default's property file's name
	 * @return the properties object found. This result cannot be <tt>null</tt>
	 */
	public static Properties getProcessedProperties(final Class<?> clazz,
			final String filename) throws IOException {

		nonNullArgument(clazz, "class");

		return getProcessedProperties(clazz.getClassLoader(), filename);
	}

	/**
	 * return a {@link Properties} object, found either amongst the System
	 * properties, or in a property file. If the property is still of the form "
	 * <tt>${...}</tt>", this method raises an exception.
	 * 
	 * @param filename
	 *            the default's property file's name
	 * @return the properties object found. This result cannot be <tt>null</tt>
	 */
	public static Properties getProcessedProperties(final String filename)
			throws IOException {

		return getProcessedProperties(currentThread().getContextClassLoader(),
				filename);
	}

	/**
	 * return a {@link Properties} object, found either amongst the System
	 * properties, or in a property file. If the property is still of the form "
	 * <tt>${...}</tt>", this method raises an exception.
	 * 
	 * @param classLoader
	 *            the {@link ClassLoader} to use to load the property file
	 * @param filename
	 *            the default's property file's name
	 * @return the properties object found. This result cannot be <tt>null</tt>
	 */
	private static Properties getProcessedProperties(
			final ClassLoader classLoader, final String filename)
			throws IOException {

		nonNullArgument(classLoader, "classLoader");
		nonNullArgument(filename, "filename");

		final InputStream is = classLoader.getResourceAsStream(filename);

		if (is == null) {
			throw new FileNotFoundException("Cannot find property file: "
					+ filename);
		}

		try {

			return getProcessedProperties(is);

		} finally {
			is.close();
		}
	}

	/**
	 * return a {@link Properties} object, found either amongst the System
	 * properties, or in a property file. If the property is still of the form "
	 * <tt>${...}</tt>", this method raises an exception.
	 * 
	 * @param is
	 *            the stream to read.
	 * @return the properties object found. This result cannot be <tt>null</tt>.
	 */
	public static Properties getProcessedProperties(final InputStream is)
			throws IOException {

		nonNullArgument(is, "inputStream");

		final Reader reader = new InputStreamReader(is, UTF_8);
		try {

			return getProcessedProperties(reader);

		} finally {
			reader.close();
		}
	}

	/**
	 * return a {@link Properties} object, found either amongst the System
	 * properties, or in a property file. If the property is still of the form "
	 * <tt>${...}</tt>", this method raises an exception.
	 * 
	 * @param reader
	 *            the stream to read.
	 * @return the properties object found. This result cannot be <tt>null</tt>.
	 */
	public static Properties getProcessedProperties(final Reader reader)
			throws IOException {

		nonNullArgument(reader, "reader");
		
		final Properties finalProperties = new Properties(
				System.getProperties());

		final Properties p = new Properties();

		p.load(reader);

		for (final Enumeration<?> e = p.propertyNames(); e.hasMoreElements();) {

			final String key = (String) e.nextElement();

			if (finalProperties.getProperty(key) == null) {

				final String value = p.getProperty(key);

				assertPropertyValueHasBeenProcessed(key, value);

				finalProperties.setProperty(key, value);
			}
		}

		return finalProperties;
	}

	/**
	 * throw an exception if the property's value has apparently not been
	 * processed by Maven.
	 * 
	 * @param key
	 *            the property's key
	 * @param value
	 *            the property's value
	 * @throws IllegalStateException
	 *             if the property is still of the form " <tt>${...}</tt>".
	 */
	private static void assertPropertyValueHasBeenProcessed(
			@Nullable final String key, @Nullable final String value)
			throws IllegalStateException {

		if (value != null && value.contains("${")) {

			throw new IllegalStateException("Property value \"" + value
					+ "\" has not been processed for key: \"" + key + "\"");
		}
	}

	/**
	 * filter a text with properties. That is, "<tt>abc${TOTO}def</tt>" will be
	 * filtered with the value of the property "<tt>TOTO</tt>"...
	 * 
	 * @param s
	 *            the text to filter
	 * @param properties
	 *            the filtering properties
	 * @return the filtered text
	 */
	@Nullable
	public static String filterEnvironmentVariables(@Nullable final String s,
			@Nullable final Properties properties) {

		if (s == null) {
			return null;
		}

		if (!s.contains("${")) {
			return s;
		}

		logDebugFormat(logger, "Filtering property: %s...", s);

		final int start = s.indexOf("${");
		final int end = s.indexOf("}", start);

		if (end == -1) {
			throw new IllegalArgumentException("Cannot filter \"${...}\" in: "
					+ s);
		}

		final String varname = s.substring(start + 2, end);

		final String value;

		final String systemProperty = System.getProperty(varname);

		if (properties != null) {

			final String varvalue = properties.getProperty(varname);

			value = varvalue == null ? systemProperty : varvalue;

		} else {

			value = systemProperty;
		}

		if (value == null) {
			throw new RuntimeException("Cannot load environment variable: "
					+ varname);
		}

		if (value.contains("${")) {
			throw new RuntimeException("Property " + varname
					+ " has not been processed: " + value);
		}

		final StringBuilder filtered = new StringBuilder();

		filtered.append(s.substring(0, start));
		filtered.append(value);
		filtered.append(s.substring(end + 1));

		final String result = filterEnvironmentVariables(filtered.toString(),
				properties);

		logDebugFormat(logger, "Filtered property is: %s.", s);

		return result;
	}

	/**
	 * return <tt>true</tt> if two {@link Properties} objects are equal.
	 * 
	 * @param p1
	 *            the first {@link Properties} object to compare
	 * @param p2
	 *            the second {@link Properties} object to compare
	 * @return <tt>true</tt> if p1 and p2 are equal
	 */
	public static boolean propertiesEqual(@Nullable final Properties p1,
			@Nullable final Properties p2) {

		if (p1 == null && p2 == null) {

			return true;
		}

		if (p1 == null || p2 == null) {

			return false;
		}

		for (final Enumeration<?> e = p1.keys(); e.hasMoreElements();) {

			final String key = (String) e.nextElement();

			final String value1 = p1.getProperty(key);
			final String value2 = p2.getProperty(key);

			if (!StringUtils.equals(value1, value2)) {
				return false;
			}
		}

		for (final Enumeration<?> e = p2.keys(); e.hasMoreElements();) {

			final String key = (String) e.nextElement();

			final String value1 = p1.getProperty(key);
			final String value2 = p2.getProperty(key);

			if (!StringUtils.equals(value1, value2)) {
				return false;
			}
		}

		return true;
	}
}
